
Internal Family Systems (IFS) therapy is a psychotherapeutic model that helps individuals understand and navigate their inner world by identifying and interacting with different sub-personalities, or "parts." In this project I worked with an IFS Level 3 certified therapist and my business partner to develop a web application that enhances this therapeutic process by allowing users to map their "Internal Systems". The project took around six months and is now in open beta. Throughout this project, I wore many hats, but my primary role was as the engineer and developer. I used Next.js, Supabase, Reactflow, and Sanity.io for the core features of the web application.
The application file hierarchy was fairly straightforward.


I chose Next.js for its SSR capabilities, particularly for the blog portion of the site, where content is pulled from an external CMS (Sanity.io). To achieve this, I developed a page.tsx component that fetches data from Sanity before rendering.
TSX1export const metadata = getSEOTags({2 title: `Blog | ${config.appName}`,3 canonicalUrlRelative: "/blog",4 description: "Sharing app progress, personal journeys, and more.",5});67export default async function Page() {8 const posts = await sanityFetch<SanityDocument[]>({9 query: postsQuery,10 tags: ["post"],11 });12 const isDraftMode = isDraftModeTrue();13 if (isDraftMode && token) {14 console.log("previewing content");15 return (16 <main className="flex flex-col gap-12 px-8 py-24">17 <PreviewProvider token={token}>18 <PreviewPosts posts={posts} />19 </PreviewProvider>20 </main>21 );22 }23 return (24 <main className="flex flex-col gap-12 px-8 py-24">25 <Posts posts={posts} />26 </main>27 );28}
Another key part of the application was user authentication, which I implemented using Supabase middleware and a layout.tsx component.
TSX1export default async function LayoutPrivate({2 children,3}: {4 children: ReactNode;5}) {6 const supabase = await createClient();7 const {8 data: { user },9 } = await supabase.auth.getUser();10 if (!user) {11 redirect("/");12 }13 ...
For the core functionality—namely, user parts/network mapping—I used Reactflow.dev for rendering nodes and edges, along with React Query for server synchronization.
TSX1if (userParts.data) {2 return (3 <div4 className="reactflow-wrapper h-full w-full overscroll-contain"5 ref={reactFlowWrapper}6 >7 <ReactFlow8 nodeTypes={nodeTypes}9 onInit={tick}10 edgeTypes={edgeTypes}11 deleteKeyCode={["Backspace", "Delete"]}12 nodes={nodes}13 edges={edges}14 onNodesChange={onNodesChange}15 onEdgesChange={onEdgesChange}16 onDragOver={onDragOver}17 onDrop={onDrop}18 zoomOnDoubleClick={false}19 zoomOnPinch={true}20 connectionLineType={ConnectionLineType.Straight}21 connectionLineStyle={{22 stroke: `oklch(var(--ec)) !important`,23 strokeWidth: 3,24 }}25 defaultEdgeOptions={{26 style: { strokeWidth: 3, stroke: `oklch(var(--ec)) !important` },27 type: "contextFloatingEdge",28 label: "Protecting",29 data: "protective_relationships",30 markerEnd: {31 type: MarkerType.ArrowClosed,32 },33 }}34 onConnect={onConnect}35 onEdgesDelete={(e) => {36 for (const edge of e) {37 deleteRelationship.mutate(edge.id);38 }39 }}40 onNodesDelete={(n) => {41 deletePartsFromContext.mutate(42 n.map((node) => {43 return { part_id: node.id, context_id: context_id };44 }),45 );46 }}47 panOnScroll48 panOnDrag={[1, 2]}49 selectionOnDrag={true}50 selectionMode={SelectionMode.Partial}51 onPaneClick={(e) => {52 if (e.ctrlKey) {53 addNewPart(e);54 } else {55 mapStore.setSelectedPartId(null);56 mapStore.setSelectedInto(null);57 }58 }}5960 ...
For designing the React Query hooks, I followed the guidelines set up in TkDodo's blog (https://tkdodo.eu/blog/practical-react-query).
The database was also a crucial part of the project. Getting the table structures and relationships right early on was essential to avoid constant redesigns as new requirements emerged.
I primarily used Supabase for its convenient JavaScript library for querying, but most of the table and view design was done purely through SQL.

For example, the enriched_feelings_tree view was built to provide a data-drillable tree for user surveys.
SQL1create view2 public.enriched_feelings_tree as3with recursive4 t as (5 select6 f.id,7 null::bigint as parent_id,8 0 as depth9 from10 feelings f11 join feelings_tree ft on ft.feeling_id = f.id12 and ft.parent_id is null13 union all14 select15 ft.feeling_id as id,16 ft.parent_id,17 t_1.depth + 118 from19 feelings_tree ft20 join t t_1 on t_1.id = ft.parent_id21 )22select23 f1.id as feeling_id,24 f1.nvc,25 f1.feeling,26 f2.id as parent_feeling_id,27 t.depth28from29 t30 left join feelings f1 on f1.id = t.id31 left join feelings f2 on f2.id = t.parent_id;
This project was more than just an exercise in software development—it was an opportunity to contribute to a tool that could make a meaningful impact on therapeutic practices. By integrating modern web technologies with the IFS model, I helped create a tool that enhances self-exploration and healing. Seeing the application go from concept to open beta has been incredibly rewarding, and I look forward to how it evolves as more users engage with it.